﻿using FB.Helpers;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Data.Entity.Core.Objects;

using FB.Annotations;
using FB.Extensions;
using ServiceStack.Text;

namespace FB.Data.Models
{
    public partial class DbContextBase : DbContext
    {
        NLog.Logger _logger = null;

        public DbSet<TrackChangeLog> TrackChangelogs { get; set; }
        public DbSet<Log> Logs { get; set; }
        


        public DbContextBase() {  }
        public DbContextBase(string NameOrConnectionString)
            : base(NameOrConnectionString)
        {
            ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized += this.OnObjectMaterialized;
            Initial();
        }

        void Initial()
        {
            _logger = NLog.LogManager.GetCurrentClassLogger();
            #region sql Debuging
#if DEBUG
            var log = new FB.Log.EFDebuger();
            this.Database.Log = log.Log;
#endif
            #endregion
        }


        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            FB.Annotations.PrecisionAttribute.ConfigureModelBuilder(modelBuilder);
        }


        public override int SaveChanges()
        {
                return SaveChanges(false);
        }

        /// <summary>
        /// Saves all changes made in this context to the underlying database.
        /// </summary>
        /// <param name="AutoRefresh"></param>
        /// <returns>The number of objects written to the underlying database.</returns>
        public int SaveChanges(bool AutoRefresh)
        {
            SetProtectedData();
            SetAuditValues();
            MakeTrackerLog();

            int RowCount = 0;
            bool saveFailed = false;


            do
            {
                try
                {
                    saveFailed = false;
                    RowCount = base.SaveChanges();
                }
                #region Catch Error
                #region for debuging
                catch (DbEntityValidationException dbEx)
                {
                    StringBuilder validationExMsg = new StringBuilder();
                    foreach (var validationErrors in dbEx.EntityValidationErrors)
                    {
                        foreach (var validationError in validationErrors.ValidationErrors)
                        {
                            validationExMsg.AppendFormat("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);
                            validationExMsg.AppendLine();
                        }
                    }

                    _logger.Error(validationExMsg.ToString());

#if(DEBUG)
                    {
                        throw dbEx;
                    }
#endif
                }
                #endregion
                catch (OptimisticConcurrencyException ocEx)
                {
                    // concurrency violation
                    RowCount = -1;

                    _logger.Error(ocEx.ToString());

#if(DEBUG)
                    throw ocEx;
#endif
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    saveFailed = true;
                    // concurrency update error
                    //RowCount = -2;
                    var entry = ex.Entries.Single();
                    entry.OriginalValues.SetValues(entry.GetDatabaseValues());

                    _logger.Error(ex.ToString());

#if(DEBUG)
                    throw ex;
#endif
                }
                catch (Exception e)
                {
                    _logger.Error(e);
#if(DEBUG)
                    throw e;
#endif
                }
            } while (saveFailed);
            #endregion


            if (AutoRefresh)
                Refresh();
            
            return RowCount;
        }


        public void Refresh()
        {
            var entries = this.ChangeTracker.Entries()
                .Where(e => (e.State == EntityState.Unchanged)).Select(s => s.Entity);

            foreach (var entry in entries)
                base.Entry(entry).Reload();
        }


        public void OnObjectMaterialized(object sender, ObjectMaterializedEventArgs e)
        {
            # region Fill Data
            if (e.Entity is ISecuredEntity)
            {
                var keyObject = GetSecurityKey();
                var entity = e.Entity;
                Type t = entity.GetType();

                var dataString = ((ISecuredEntity)entity).ProtectedDataSource;

                if (((ISecuredEntity)entity).IsEncrypted && null != keyObject && keyObject.ValidateUser())
                {
                    dataString = dataString.Decrypt(keyObject.GetPassKey());
                }

                if (
                    (!((ISecuredEntity)entity).IsEncrypted) ||
                    (((ISecuredEntity)entity).IsEncrypted && null != keyObject && keyObject.ValidateUser())
                    )
                {
                    try
                    {
                        var dataStore = TypeSerializer.DeserializeFromString<Dictionary<string, object>>(dataString);

                        foreach (var k in dataStore.Keys)
                        {
                            if (null != dataStore[k])
                            {
                                var propInfo = t.GetProperty(k);
                                if (null != propInfo)
                                {
                                    propInfo.SetValue(e.Entity, TypeSerializer.DeserializeFromString(dataStore[k].ToString(), propInfo.PropertyType), null);
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.ErrorException("Deserialization Error. ", ex);
                    }
                }
            }
            #endregion
        }



        void MakeTrackerLog()
        {
            //var user = string.Empty;
            //if (System.Web.HttpContext.Current != null &&
            //    System.Web.HttpContext.Current.User.Identity.IsAuthenticated)
            //{
            //    user = System.Web.HttpContext.Current.User.Identity.Name;
            //}

            var user = GetCurrentUser();

            var modifiedEntries = this.ChangeTracker.Entries();

            var statusToLog = new[] { EntityState.Added, EntityState.Deleted, EntityState.Modified };

            try
            {
                modifiedEntries.ToList().ForEach(e =>
                {
                    var log = new TrackChangeLog();
                    if (statusToLog.Contains(e.State))
                    {
                        log.User = user;
                        log.Date = DateTime.Now;
                        log.Table = this.GetTableName(e);

                        log.ObjectType = String.Format("{0}, {1}",
                            e.Entity.GetType().FullName,
                            e.Entity.GetType().Assembly);

                        dynamic obj = new object();

                        switch (e.State)
                        {
                            case EntityState.Added:
                                log.Action = DBAction.Insert;
                                obj = e.CurrentValues.ToObject();
                                break;
                            case EntityState.Modified:
                                log.Action = DBAction.Update;
                                obj = e.OriginalValues.ToObject();
                                break;
                            case EntityState.Deleted:
                                log.Action = DBAction.Delete;
                                obj = e.OriginalValues.ToObject();
                                break;
                        }


                        log.ActionDesc = EnumHelper.GetDescription(log.Action);

                        if (e.Entity is ISecuredEntity)
                        {
                            // skip the protected properties
                            Type t = e.Entity.GetType();
                            var properties = GetSecuredDataProperty(t);
                            if (null != properties)
                            {
                                foreach (var p in properties)
                                {
                                    var propertyType = p.PropertyType;
                                    p.SetValue(obj, GetDefault(propertyType), null);
                                }
                            }
                        }

                        log.SerializedData = JsonConvert.SerializeObject(obj, new JsonSerializerSettings
                        {
                            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                        });

                        this.TrackChangelogs.Add(log);
                    }
                });
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }

        }


        private void SetAuditValues()
        {
            string currentUser = GetCurrentUser();


            #if(DEBUG)
            // for debuger
            if (string.IsNullOrEmpty(currentUser)) currentUser = "debuger";
            #endif

            

            if (!string.IsNullOrEmpty(currentUser))
            {
                #region Inserting case

                // approval log
                var approvalLogEntries = this.ChangeTracker.Entries()
                    .Where(e => e.State == EntityState.Added).Select(s => s.Entity).OfType<IApprovalLog>();
                    

                foreach (var entry in approvalLogEntries)
                {
                    entry.ApprovedDate = DateTime.Now;
                    entry.ApprovedBy = currentUser;
                }


                var addedEntries = this.ChangeTracker.Entries()
                    .Where(e => e.State == EntityState.Added).Select(s => s.Entity).OfType<FBAuditModel>();
                    

                foreach (var entry in addedEntries)
                {
                    
                    entry.CreatedDate = DateTime.Now;
                    entry.CreatedBy = currentUser;
                    entry.ModifiedDate = DateTime.Now;
                    entry.ModifiedBy = currentUser;
                }

                #endregion

                #region Updating case

                var modifiedEntries = this.ChangeTracker.Entries()
                    .Where(e => e.State == EntityState.Modified).Select(s => s)
                    //.Select(s => s.Entity).OfType<FBAuditModel>()
                    ;

                foreach (var entry in modifiedEntries)
                {
                    if (entry.Entity is FBAuditModel)
                    {
                        var u = (FBAuditModel)entry.Entity;
                        u.ModifiedDate = DateTime.Now;
                        u.ModifiedBy = currentUser;

                        entry.Cast<FBAuditModel>().Property(p => p.CreatedBy).IsModified = false;
                        entry.Cast<FBAuditModel>().Property(p => p.CreatedDate).IsModified = false;
                    }
                }

                #endregion
            }
            //else
            //{
            //    throw new NoNullAllowedException("Current login username cant be empty");
            //}
        }


        void SetProtectedData()
        {
            var securedEntries = this.ChangeTracker.Entries()
                .Where(e => (e.State == EntityState.Added || e.State == EntityState.Modified)).Select(s => s.Entity).OfType<ISecuredEntity>();

            if (securedEntries.Count() > 0)
            {
                var keyObject = GetSecurityKey();

                foreach (var entry in securedEntries)
                {
                    Type t = entry.GetType();

                    var properties = GetSecuredDataProperty(t);

                    #region serialize data
                    if (properties.Count() > 0)
                    {
                        var dataStore = new Dictionary<string, object>();

                        foreach (var p in properties)
                        {
                            dataStore.Add(p.Name, p.GetValue(entry, null));
                            //dataStore.Add(p.Name, TypeSerializer.SerializeToString(p.GetValue(entry, null), p.PropertyType));
                        }

                        entry.ProtectedDataSource = TypeSerializer.SerializeToString<Dictionary<string, object>>(dataStore);

                        #region encrypt data

                        if (AppSettingsBase.IsDataEncryptionEnabled)
                        {
                            if (null != keyObject && keyObject.ValidateUser())
                            {
                                entry.ProtectedDataSource = entry.ProtectedDataSource.Encrypt(keyObject.GetPassKey());
                                entry.IsEncrypted = true;
                            }
                            else
                            {
                                #region Log encyption error and throw exception

                                _logger.Error("Encryption Error. {0}", t.FullName);

                                throw new Exception("Unable to save changes. Try again, and if the problem persists contact your system administrator.");

                                #endregion
                            }
                        }
                        else
                        {
                            entry.IsEncrypted = false;
                        }

                        #endregion

                    }
                    #endregion
                }
            }

        }


        protected virtual string GetCurrentUser()
        {
            var currentUser = string.Empty;
            if (System.Web.HttpContext.Current != null &&
                System.Web.HttpContext.Current.User.Identity.IsAuthenticated)
            {
                currentUser = System.Web.HttpContext.Current.User.Identity.Name;
            }
            return currentUser;
        }

        public virtual ISecurityKey GetSecurityKey()
        {
            throw new NotImplementedException();
        }
        

        object GetDefault(Type type)
        {
            if (type.IsValueType)
            {
                return Activator.CreateInstance(type);
            }

            return null;
        }

        IEnumerable<PropertyInfo> GetSecuredDataProperty(Type type)
        {
            var properties = from p in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                             let attr = p.GetCustomAttributes(typeof(SecuredDataAttribute), true)
                             where attr.Length == 1
                             select p;

            return properties;
        }


        string GetTableName(DbEntityEntry ent)
        {
            System.Data.Entity.Core.Objects.ObjectContext objectContext = ((IObjectContextAdapter)this).ObjectContext;
            //System.Data.Objects.ObjectContext objectContext = ((IObjectContextAdapter)this).ObjectContext;
            Type entityType = ent.Entity.GetType();

            if (entityType.BaseType != null && entityType.Namespace == "System.Data.Entity.DynamicProxies")
                entityType = entityType.BaseType;

            string entityTypeName = entityType.Name;

            System.Data.Entity.Core.Metadata.Edm.EntityContainer container =
                objectContext.MetadataWorkspace.GetEntityContainer(objectContext.DefaultContainerName, 
                System.Data.Entity.Core.Metadata.Edm.DataSpace.CSpace
                );
            string entitySetName = (from meta in container.BaseEntitySets
                                    where meta.ElementType.Name == entityTypeName
                                    select meta.Name).DefaultIfEmpty().First();

            return !string.IsNullOrEmpty(entitySetName) ? entitySetName : entityTypeName;
        }



    }
}
